/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.yangfei.SportAssistant.content;

import static com.yangfei.SportAssistant.util.Constants.DEFAULT_MIN_REQUIRED_ACCURACY;
import static com.yangfei.SportAssistant.util.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS;
import static com.yangfei.SportAssistant.util.Constants.MAX_LOCATION_AGE_MS;
import static com.yangfei.SportAssistant.util.Constants.MAX_NETWORK_AGE_MS;
import static com.yangfei.SportAssistant.util.Constants.TAG;
import static com.yangfei.SportAssistant.util.Constants.TARGET_DISPLAYED_TRACK_POINTS;

import com.yangfei.SportAssistant.R;
import com.yangfei.SportAssistant.content.DataSourceManager.DataSourceListener;
import com.yangfei.SportAssistant.content.SportAssistantProviderUtils.DoubleBufferedLocationFactory;
import com.yangfei.SportAssistant.content.SportAssistantProviderUtils.LocationIterator;
import com.yangfei.SportAssistant.content.TrackDataListener.ProviderState;
import com.yangfei.SportAssistant.content.TrackDataListeners.ListenerRegistration;
import com.yangfei.SportAssistant.util.Constants;
import com.yangfei.SportAssistant.util.LocationUtils;
import com.yangfei.SportAssistant.util.PreferencesUtils;

import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.hardware.GeomagneticField;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;

import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

/**
 * Track data hub, which receives data (both live and recorded) from many
 * different sources and distributes it to those interested after some standard
 * processing.
 * 
 * TODO: Simplify the threading model here, it's overly complex and it's not
 * obvious why certain race conditions won't happen.
 * 
 * @author Rodrigo Damazio
 */
public class TrackDataHub
{

	// Preference keys
	private final String MIN_REQUIRED_ACCURACY_KEY;
	private final String METRIC_UNITS_KEY;
	private final String SPEED_REPORTING_KEY;

	// Overridable constants
	private final int targetNumPoints;

	/** Types of data that we can expose. */
	public static enum ListenerDataType
	{
		/** Listen to when the selected track changes. */
		SELECTED_TRACK_CHANGED,

		/** Listen to when the tracks change. */
		TRACK_UPDATES,

		/** Listen to when the waypoints change. */
		WAYPOINT_UPDATES,

		/** Listen to when the current track points change. */
		POINT_UPDATES,

		/**
		 * Listen to sampled-out points. Listening to this without listening to
		 * {@link #POINT_UPDATES} makes no sense and may yield unexpected results.
		 */
		SAMPLED_OUT_POINT_UPDATES,

		/** Listen to updates to the current location. */
		LOCATION_UPDATES,

		/** Listen to updates to the current heading. */
		COMPASS_UPDATES,

		/** Listens to changes in display preferences. */
		DISPLAY_PREFERENCES;
	}

	/** Listener which receives events from the system. */
	private class HubDataSourceListener implements DataSourceListener
	{
		@Override
		public void notifyTrackUpdated()
		{
			TrackDataHub.this.notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES));
		}

		@Override
		public void notifyWaypointUpdated()
		{
			TrackDataHub.this.notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES));
		}

		@Override
		public void notifyPointsUpdated()
		{
			TrackDataHub.this.notifyPointsUpdated(true, 0, 0, getListenersFor(ListenerDataType.POINT_UPDATES), getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES));
		}

		@Override
		public void notifyPreferenceChanged(String key)
		{
			TrackDataHub.this.notifyPreferenceChanged(key);
		}

		@Override
		public void notifyLocationProviderEnabled(boolean enabled)
		{
			hasProviderEnabled = enabled;
			TrackDataHub.this.notifyFixType();
		}

		@Override
		public void notifyLocationProviderAvailable(boolean available)
		{
			hasFix = available;
			TrackDataHub.this.notifyFixType();
		}

		@Override
		public void notifyLocationChanged(Location loc)
		{
			TrackDataHub.this.notifyLocationChanged(loc, getListenersFor(ListenerDataType.LOCATION_UPDATES));
		}

		@Override
		public void notifyHeadingChanged(float heading)
		{
			lastSeenMagneticHeading = heading;
			maybeUpdateDeclination();
			TrackDataHub.this.notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES));
		}
	}

	// Application services
	private final Context context;
	private final SportAssistantProviderUtils providerUtils;
	private final SharedPreferences preferences;

	// Get content notifications on the main thread, send listener callbacks in
	// another.
	// This ensures listener calls are serialized.
	private HandlerThread listenerHandlerThread;
	private Handler listenerHandler;

	/** Manager for external listeners (those from activities). */
	private final TrackDataListeners dataListeners;

	/** Wrapper for interacting with system data managers. */
	private DataSourcesWrapper dataSources = null;

	/** Manager for system data listener registrations. */
	private DataSourceManager dataSourceManager = null;

	/** Condensed listener for system data listener events. */
	private final DataSourceListener dataSourceListener = new HubDataSourceListener();

	// Cached preference values
	private int minRequiredAccuracy;
	private boolean useMetricUnits;
	private boolean reportSpeed;

	// Cached sensor readings
	private float declination;
	private long lastDeclinationUpdate;
	private float lastSeenMagneticHeading;

	// Cached GPS readings
	private Location lastSeenLocation;
	private boolean hasProviderEnabled = true;
	private boolean hasFix;
	private boolean hasGoodFix;

	// Transient state about the selected track
	private long selectedTrackId = -1;
	private long firstSeenLocationId;
	private long lastSeenLocationId;
	private int numLoadedPoints;
	private int lastSamplingFrequency;
	private DoubleBufferedLocationFactory locationFactory;

	private boolean started = false;

	/**
	 * Builds a new {@link TrackDataHub} instance.
	 */
	public synchronized static TrackDataHub newInstance(Context context)
	{
		SharedPreferences preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
		SportAssistantProviderUtils providerUtils = SportAssistantProviderUtils.Factory.get(context);
		return new TrackDataHub(context, new TrackDataListeners(), preferences, providerUtils, TARGET_DISPLAYED_TRACK_POINTS);
	}

	/**
	 * Injection constructor.
	 */
	// @VisibleForTesting
	TrackDataHub(Context ctx, TrackDataListeners listeners, SharedPreferences preferences,
			SportAssistantProviderUtils providerUtils, int targetNumPoints)
	{
		this.context = ctx;
		this.dataListeners = listeners;
		this.preferences = preferences;
		this.providerUtils = providerUtils;
		this.targetNumPoints = targetNumPoints;
		this.locationFactory = new DoubleBufferedLocationFactory();

		MIN_REQUIRED_ACCURACY_KEY = context.getString(R.string.min_required_accuracy_key);
		METRIC_UNITS_KEY = context.getString(R.string.metric_units_key);
		SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key);
		
		this.dataSources = newDataSources();
		this.dataSourceManager = new DataSourceManager(this.dataSourceListener, this.dataSources);

		resetState();
	}

	/**
	 * Starts listening to data sources and reporting the data to external
	 * listeners.
	 */
	public void start()
	{
		Log.i(TAG, "TrackDataHub.start");
		if (isStarted())
		{
			Log.w(TAG, "Already started, ignoring");
			return;
		}
		started = true;

		listenerHandlerThread = new HandlerThread("trackDataContentThread");
		listenerHandlerThread.start();
		listenerHandler = new Handler(listenerHandlerThread.getLooper());

		// This may or may not register internal listeners, depending on whether
		// we already had external listeners.
		dataSourceManager.updateAllListeners(getNeededListenerTypes());
		loadSharedPreferences();

		// If there were listeners already registered, make sure they become
		// up-to-date.
		loadDataForAllListeners();
	}

	// @VisibleForTesting
	protected DataSourcesWrapper newDataSources()
	{
		return new DataSourcesWrapperImpl(context, preferences);
	}

	/**
	 * Stops listening to data sources and reporting the data to external
	 * listeners.
	 */
	public void stop()
	{
		Log.i(TAG, "TrackDataHub.stop");
		if (!isStarted())
		{
			Log.w(TAG, "Not started, ignoring");
			return;
		}

		// Unregister internal listeners even if there are external listeners
		// registered.
		dataSourceManager.unregisterAllListeners();
		
		if( listenerHandlerThread != null )
		listenerHandlerThread.getLooper().quit();

		started = false;

		listenerHandlerThread = null;
		listenerHandler = null;
	}

	private boolean isStarted()
	{
		return started;
	}

	@Override
	protected void finalize() throws Throwable
	{
		if (isStarted() || (listenerHandlerThread != null && listenerHandlerThread.isAlive()))
		{
			Log.e(TAG, "Forgot to stop() TrackDataHub");
		}

		super.finalize();
	}

	private void loadSharedPreferences()
	{
		selectedTrackId = PreferencesUtils.getSelectedTrackId(context);
		useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true);
		reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true);
		minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, DEFAULT_MIN_REQUIRED_ACCURACY);
	}

	/** Updates known magnetic declination if needed. */
	private void maybeUpdateDeclination()
	{
		if (lastSeenLocation == null)
		{
			// We still don't know where we are.
			return;
		}

		// Update the declination every hour
		long now = System.currentTimeMillis();
		if (now - lastDeclinationUpdate < 60 * 60 * 1000)
		{
			return;
		}

		lastDeclinationUpdate = now;
		long timestamp = lastSeenLocation.getTime();
		if (timestamp == 0)
		{
			// Hack for Samsung phones which don't populate the time field
			timestamp = now;
		}

		declination = getDeclinationFor(lastSeenLocation, timestamp);
		Log.i(TAG, "Updated magnetic declination to " + declination);
	}

	// @VisibleForTesting
	protected float getDeclinationFor(Location location, long timestamp)
	{
		GeomagneticField field = new GeomagneticField((float) location.getLatitude(), (float) location.getLongitude(), (float) location.getAltitude(), timestamp);
		return field.getDeclination();
	}

	/**
	 * Forces the current location to be updated and reported to all listeners.
	 * The reported location may be from the network provider if the GPS provider
	 * is not available or doesn't have a fix.
	 */
	public void forceUpdateLocation()
	{
		if (!isStarted())
		{
			Log.w(TAG, "Not started, not forcing location update");
			return;
		}
		Log.i(TAG, "Forcing location update");

		Location loc = dataSources.getLastKnownLocation();
		if (loc != null)
		{
			notifyLocationChanged(loc, getListenersFor(ListenerDataType.LOCATION_UPDATES));
		}
	}

	/** Returns the ID of the currently-selected track. */
	public long getSelectedTrackId()
	{
		if (!isStarted())
		{
			loadSharedPreferences();
		}
		return selectedTrackId;
	}

	/** Returns whether there's a track currently selected. */
	public boolean isATrackSelected()
	{
		return getSelectedTrackId() > 0;
	}

	/** Returns whether the selected track is still being recorded. */
	public boolean isRecordingSelected()
	{
		if (!isStarted())
		{
			loadSharedPreferences();
		}
		long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
		return recordingTrackId != -1L && recordingTrackId == selectedTrackId;
	}

	/**
	 * Loads the given track and makes it the currently-selected one. It is ok to
	 * call this method before {@link #start}, and in that case the data will only
	 * be passed to listeners when {@link #start} is called.
	 * 
	 * @param trackId
	 *          the ID of the track to load
	 */
	public void loadTrack(long trackId)
	{
		if (trackId == selectedTrackId)
		{
			Log.w(TAG, "Not reloading track, id=" + trackId);
			return;
		}

		// Save the selection to memory and flush.
		selectedTrackId = trackId;
		PreferencesUtils.setSelectedTrackId(context, selectedTrackId);

		// Force it to reload data from the beginning.
		Log.d(TAG, "Loading track");
		resetState();

		loadDataForAllListeners();
	}

	/**
	 * Resets the internal state of what data has already been loaded into
	 * listeners.
	 */
	private void resetState()
	{
		firstSeenLocationId = -1;
		lastSeenLocationId = -1;
		numLoadedPoints = 0;
		lastSamplingFrequency = -1;
	}

	/**
	 * Unloads the currently-selected track.
	 */
	public void unloadCurrentTrack()
	{
		loadTrack(-1);
	}

	public void registerTrackDataListener(TrackDataListener listener, EnumSet<ListenerDataType> dataTypes)
	{
		synchronized (dataListeners)
		{
			ListenerRegistration registration = dataListeners.registerTrackDataListener(listener, dataTypes);

			// Don't load any data or start internal listeners if start() hasn't been
			// called. When it is called, we'll do both things.
			if (!isStarted())
				return;

			loadNewDataForListener(registration);

			dataSourceManager.updateAllListeners(getNeededListenerTypes());
		}
	}

	public void unregisterTrackDataListener(TrackDataListener listener)
	{
		synchronized (dataListeners)
		{
			dataListeners.unregisterTrackDataListener(listener);

			// Don't load any data or start internal listeners if start() hasn't been
			// called. When it is called, we'll do both things.
			if (!isStarted())
				return;

			dataSourceManager.updateAllListeners(getNeededListenerTypes());
		}
	}

	/**
	 * Reloads all track data received so far into the specified listeners.
	 */
	public void reloadDataForListener(TrackDataListener listener)
	{
		ListenerRegistration registration;
		synchronized (dataListeners)
		{
			registration = dataListeners.getRegistration(listener);
			registration.resetState();
			loadNewDataForListener(registration);
		}
	}

	/**
	 * Reloads all track data received so far into the specified listeners.
	 * 
	 * Assumes it's called from a block that synchronizes on
	 * {@link #dataListeners}.
	 */
	private void loadNewDataForListener(final ListenerRegistration registration)
	{
		if (!isStarted())
		{
			Log.w(TAG, "Not started, not reloading");
			return;
		}
		if (registration == null)
		{
			Log.w(TAG, "Not reloading for null registration");
			return;
		}

		// If a listener happens to be added after this method but before the
		// Runnable below is
		// executed, it will have triggered a separate call to load data only up to
		// the point this
		// listener got to. This is ensured by being synchronized on listeners.
		final boolean isOnlyListener = (dataListeners.getNumListeners() == 1);

		runInListenerThread(new Runnable()
		{
			@SuppressWarnings("unchecked")
			@Override
			public void run()
			{
				// Reload everything if either it's a different track, or the track has
				// been resampled
				// (this also covers the case of a new registration).
				boolean reloadAll = registration.lastTrackId != selectedTrackId
						|| registration.lastSamplingFrequency != lastSamplingFrequency;
				Log.d(TAG, "Doing a " + (reloadAll ? "full" : "partial") + " reload for " + registration);

				TrackDataListener listener = registration.listener;
				Set<TrackDataListener> listenerSet = Collections.singleton(listener);

				if (registration.isInterestedIn(ListenerDataType.DISPLAY_PREFERENCES))
				{
					reloadAll |= listener.onUnitsChanged(useMetricUnits);
					reloadAll |= listener.onReportSpeedChanged(reportSpeed);
				}

				if (reloadAll && registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED))
				{
					notifySelectedTrackChanged(selectedTrackId, listenerSet);
				}

				if (registration.isInterestedIn(ListenerDataType.TRACK_UPDATES))
				{
					notifyTrackUpdated(listenerSet);
				}

				boolean interestedInPoints = registration.isInterestedIn(ListenerDataType.POINT_UPDATES);
				boolean interestedInSampledOutPoints = registration.isInterestedIn(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
				if (interestedInPoints || interestedInSampledOutPoints)
				{
					long minPointId = 0;
					int previousNumPoints = 0;

					if (reloadAll)
					{
						// Clear existing points and send them all again
						notifyPointsCleared(listenerSet);
					}
					else
					{
						// Send only new points
						minPointId = registration.lastPointId + 1;
						previousNumPoints = registration.numLoadedPoints;
					}

					// If this is the only listener we have registered, keep the state
					// that we serve to it as
					// a reference for other future listeners.
					if (isOnlyListener && reloadAll)
					{
						resetState();
					}

					notifyPointsUpdated(isOnlyListener, minPointId, previousNumPoints, listenerSet, interestedInSampledOutPoints ? listenerSet
							: Collections.EMPTY_SET);
				}

				if (registration.isInterestedIn(ListenerDataType.WAYPOINT_UPDATES))
				{
					notifyWaypointUpdated(listenerSet);
				}

				if (registration.isInterestedIn(ListenerDataType.LOCATION_UPDATES))
				{
					if (lastSeenLocation != null)
					{
						notifyLocationChanged(lastSeenLocation, true, listenerSet);
					}
					else
					{
						notifyFixType();
					}
				}

				if (registration.isInterestedIn(ListenerDataType.COMPASS_UPDATES))
				{
					notifyHeadingChanged(listenerSet);
				}
			}
		});
	}

	/**
	 * Reloads all track data received so far into the specified listeners.
	 */
	private void loadDataForAllListeners()
	{
		if (!isStarted())
		{
			Log.w(TAG, "Not started, not reloading");
			return;
		}
		synchronized (dataListeners)
		{
			if (!dataListeners.hasListeners())
			{
				Log.d(TAG, "No listeners, not reloading");
				return;
			}
		}

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				// Ignore the return values here, we're already sending the full data
				// set anyway
				for (TrackDataListener listener : getListenersFor(ListenerDataType.DISPLAY_PREFERENCES))
				{
					listener.onUnitsChanged(useMetricUnits);
					listener.onReportSpeedChanged(reportSpeed);
				}

				notifySelectedTrackChanged(selectedTrackId, getListenersFor(ListenerDataType.SELECTED_TRACK_CHANGED));

				notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES));

				Set<TrackDataListener> pointListeners = getListenersFor(ListenerDataType.POINT_UPDATES);
				Set<TrackDataListener> sampledOutPointListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
				notifyPointsCleared(pointListeners);
				notifyPointsUpdated(true, 0, 0, pointListeners, sampledOutPointListeners);

				notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES));

				if (lastSeenLocation != null)
				{
					notifyLocationChanged(lastSeenLocation, true, getListenersFor(ListenerDataType.LOCATION_UPDATES));
				}
				else
				{
					notifyFixType();
				}

				notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES));
			}
		});
	}

	/**
	 * Called when a preference changes.
	 * 
	 * @param key
	 *          the key to the preference that changed
	 */
	private void notifyPreferenceChanged(String key)
	{
		if (MIN_REQUIRED_ACCURACY_KEY.equals(key))
		{
			minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, DEFAULT_MIN_REQUIRED_ACCURACY);
		}
		else if (METRIC_UNITS_KEY.equals(key))
		{
			useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true);
			notifyUnitsChanged();
		}
		else if (SPEED_REPORTING_KEY.equals(key))
		{
			reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true);
			notifySpeedReportingChanged();
		}
		else if (PreferencesUtils.getSelectedTrackIdKey(context).equals(key))
		{
			loadTrack(PreferencesUtils.getSelectedTrackId(context));
		}
	}

	/** Called when the speed/pace reporting preference changes. */
	private void notifySpeedReportingChanged()
	{
		if (!isStarted())
			return;

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				Set<TrackDataListener> displayListeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES);

				for (TrackDataListener listener : displayListeners)
				{
					// TODO: Do the reloading just once for all interested listeners
					if (listener.onReportSpeedChanged(reportSpeed))
					{
						synchronized (dataListeners)
						{
							reloadDataForListener(listener);
						}
					}
				}
			}
		});
	}

	/** Called when the metric units setting changes. */
	private void notifyUnitsChanged()
	{
		if (!isStarted())
			return;

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				Set<TrackDataListener> displayListeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES);

				for (TrackDataListener listener : displayListeners)
				{
					if (listener.onUnitsChanged(useMetricUnits))
					{
						synchronized (dataListeners)
						{
							reloadDataForListener(listener);
						}
					}
				}
			}
		});
	}

	/** Notifies about the current GPS fix state. */
	private void notifyFixType()
	{
		final TrackDataListener.ProviderState state;
		if (!hasProviderEnabled)
		{
			state = ProviderState.DISABLED;
		}
		else if (!hasFix)
		{
			state = ProviderState.NO_FIX;
		}
		else if (!hasGoodFix)
		{
			state = ProviderState.BAD_FIX;
		}
		else
		{
			state = ProviderState.GOOD_FIX;
		}

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				// Notify to everyone.
				Log.d(TAG, "Notifying fix type: " + state);
				for (TrackDataListener listener : getListenersFor(ListenerDataType.LOCATION_UPDATES))
				{
					listener.onProviderStateChange(state);
				}
			}
		});
	}

	/**
	 * Notifies the the current location has changed, without any filtering. If
	 * the state of GPS fix has changed, that will also be reported.
	 * 
	 * @param location
	 *          the current location
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyLocationChanged(Location location, Set<TrackDataListener> listeners)
	{
		notifyLocationChanged(location, false, listeners);
	}

	/**
	 * Notifies that the current location has changed, without any filtering. If
	 * the state of GPS fix has changed, that will also be reported.
	 * 
	 * @param location
	 *          the current location
	 * @param forceUpdate
	 *          whether to force the notifications to happen
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyLocationChanged(Location location, boolean forceUpdate, final Set<TrackDataListener> listeners)
	{
		if (location == null)
			return;
		if (listeners.isEmpty())
			return;

		boolean isGpsLocation = location.getProvider().equals(LocationManager.GPS_PROVIDER);

		boolean oldHasFix = hasFix;
		boolean oldHasGoodFix = hasGoodFix;

		long now = System.currentTimeMillis();
		if (isGpsLocation)
		{
			// We consider a good fix to be a recent one with reasonable accuracy.
			hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS);
			hasGoodFix = (location.getAccuracy() <= minRequiredAccuracy);
		}
		else
		{
			if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS))
			{
				// This is a network location, but we have a recent/valid GPS location,
				// just ignore this.
				return;
			}

			// We haven't gotten a GPS location in a while (or at all), assume we have
			// no fix anymore.
			hasFix = false;
			hasGoodFix = false;

			// If the network location is recent, we'll use that.
			if (isLocationOld(location, now, MAX_NETWORK_AGE_MS))
			{
				// Alas, we have no clue where we are.
				location = null;
			}
		}

		if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate)
		{
			notifyFixType();
		}

		lastSeenLocation = location;
		final Location finalLoc = location;
		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				for (TrackDataListener listener : listeners)
				{
					listener.onCurrentLocationChanged(finalLoc);
				}
			}
		});
	}

	/**
	 * Returns true if the given location is either invalid or too old.
	 * 
	 * @param location
	 *          the location to test
	 * @param now
	 *          the current timestamp in milliseconds
	 * @param maxAge
	 *          the maximum age in milliseconds
	 * @return true if it's invalid or too old, false otherwise
	 */
	private static boolean isLocationOld(Location location, long now, long maxAge)
	{
		return !LocationUtils.isValidLocation(location) || now - location.getTime() > maxAge;
	}

	/**
	 * Notifies that the current heading has changed.
	 * 
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyHeadingChanged(final Set<TrackDataListener> listeners)
	{
		if (listeners.isEmpty())
			return;

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				float heading = lastSeenMagneticHeading + declination;
				for (TrackDataListener listener : listeners)
				{
					listener.onCurrentHeadingChanged(heading);
				}
			}
		});
	}

	/**
	 * Notifies that a new track has been selected..
	 * 
	 * @param trackId
	 *          the new selected track
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifySelectedTrackChanged(long trackId, final Set<TrackDataListener> listeners)
	{
		if (listeners.isEmpty())
			return;

		Log.i(TAG, "New track selected, id=" + trackId);
		final Track track = providerUtils.getTrack(trackId);

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				for (TrackDataListener listener : listeners)
				{
					listener.onSelectedTrackChanged(track, isRecordingSelected());
				}
			}
		});
	}

	/**
	 * Notifies that the currently-selected track's data has been updated.
	 * 
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyTrackUpdated(final Set<TrackDataListener> listeners)
	{
		if (listeners.isEmpty())
			return;

		final Track track = providerUtils.getTrack(selectedTrackId);

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				for (TrackDataListener listener : listeners)
				{
					listener.onTrackUpdated(track);
				}
			}
		});
	}

	/**
	 * Notifies that waypoints have been updated. We assume few waypoints, so we
	 * reload them all every time.
	 * 
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyWaypointUpdated(final Set<TrackDataListener> listeners)
	{
		if (listeners.isEmpty())
			return;

		// Always reload all the waypoints.
		final Cursor cursor = providerUtils.getWaypointsCursor(selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS);

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				Log.d(TAG, "Reloading waypoints");
				for (TrackDataListener listener : listeners)
				{
					listener.clearWaypoints();
				}

				try
				{
					if (cursor != null && cursor.moveToFirst())
					{
						do
						{
							Waypoint waypoint = providerUtils.createWaypoint(cursor);
							if (!LocationUtils.isValidLocation(waypoint.getLocation()))
							{
								continue;
							}

							for (TrackDataListener listener : listeners)
							{
								listener.onNewWaypoint(waypoint);
							}
						}
						while (cursor.moveToNext());
					}
				}
				finally
				{
					if (cursor != null)
					{
						cursor.close();
					}
				}

				for (TrackDataListener listener : listeners)
				{
					listener.onNewWaypointsDone();
				}
			}
		});
	}

	/**
	 * Tells listeners to clear the current list of points.
	 * 
	 * @param listeners
	 *          the listeners to notify
	 */
	private void notifyPointsCleared(final Set<TrackDataListener> listeners)
	{
		if (listeners.isEmpty())
			return;

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				for (TrackDataListener listener : listeners)
				{
					listener.clearTrackPoints();
				}
			}
		});
	}

	/**
	 * Notifies the given listeners about track points in the given ID range.
	 * 
	 * @param keepState
	 *          whether to load and save state about the already-notified points.
	 *          If true, only new points are reported. If false, then the whole
	 *          track will be loaded, without affecting the state.
	 * @param minPointId
	 *          the first point ID to notify, inclusive, or 0 to determine from
	 *          internal state
	 * @param previousNumPoints
	 *          the number of points to assume were previously loaded for these
	 *          listeners, or 0 to assume it's the kept state
	 */
	private void notifyPointsUpdated(final boolean keepState, final long minPointId, final int previousNumPoints,
			final Set<TrackDataListener> sampledListeners, final Set<TrackDataListener> sampledOutListeners)
	{
		if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty())
			return;

		runInListenerThread(new Runnable()
		{
			@Override
			public void run()
			{
				notifyPointsUpdatedSync(keepState, minPointId, previousNumPoints, sampledListeners, sampledOutListeners);
			}
		});
	}

	/**
	 * Synchronous version of the above method.
	 */
	private void notifyPointsUpdatedSync(boolean keepState, long minPointId, int previousNumPoints,
			Set<TrackDataListener> sampledListeners, Set<TrackDataListener> sampledOutListeners)
	{
		// If we're loading state, start from after the last seen point up to the
		// last recorded one
		// (all new points)
		// If we're not loading state, then notify about all the previously-seen
		// points.
		if (minPointId <= 0)
		{
			minPointId = keepState ? lastSeenLocationId + 1 : 0;
		}
		long maxPointId = keepState ? -1 : lastSeenLocationId;

		// TODO: Move (re)sampling to a separate class.
		if (numLoadedPoints >= targetNumPoints)
		{
			// We're about to exceed the maximum desired number of points, so reload
			// the whole track with fewer points (the sampling frequency will be
			// lower). We do this for every listener even if we were loading just for
			// a few of them (why miss the oportunity?).

			Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points.");
			resetState();
			synchronized (dataListeners)
			{
				sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES);
				sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
			}
			maxPointId = -1;
			minPointId = 0;
			previousNumPoints = 0;
			keepState = true;

			for (TrackDataListener listener : sampledListeners)
			{
				listener.clearTrackPoints();
			}
		}

		// Keep the originally selected track ID so we can stop if it changes.
		long currentSelectedTrackId = selectedTrackId;

		// If we're ignoring state, start from the beginning of the track
		int localNumLoadedPoints = previousNumPoints;
		if (previousNumPoints <= 0)
		{
			localNumLoadedPoints = keepState ? numLoadedPoints : 0;
		}
		long localFirstSeenLocationId = keepState ? firstSeenLocationId : -1;
		long localLastSeenLocationId = minPointId;
		long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId);
		int pointSamplingFrequency = -1;

		LocationIterator it = providerUtils.getLocationIterator(currentSelectedTrackId, minPointId, false, locationFactory);

		while (it.hasNext())
		{
			if (currentSelectedTrackId != selectedTrackId)
			{
				// The selected track changed beneath us, stop.
				break;
			}

			Location location = it.next();
			long locationId = it.getLocationId();

			// If past the last wanted point, stop.
			// This happens when adding a new listener after data has already been
			// loaded,
			// in which case we only want to bring that listener up to the point where
			// the others
			// were. In case it does happen, we should be wasting few points (only the
			// ones not
			// yet notified to other listeners).
			if (maxPointId > 0 && locationId > maxPointId)
			{
				break;
			}

			if (localFirstSeenLocationId == -1)
			{
				// This was our first point, keep its ID
				localFirstSeenLocationId = locationId;
			}

			if (pointSamplingFrequency == -1)
			{
				// Now we already have at least one point, calculate the sampling
				// frequency.
				// It should be noted that a non-obvious consequence of this sampling is
				// that
				// no matter how many points we get in the newest batch, we'll never
				// exceed
				// MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before
				// resampling.
				long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId;
				numTotalPoints = Math.max(0L, numTotalPoints);
				pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints);
			}

			notifyNewPoint(location, locationId, lastStoredLocationId, localNumLoadedPoints, pointSamplingFrequency, sampledListeners, sampledOutListeners);

			localNumLoadedPoints++;
			localLastSeenLocationId = locationId;
		}
		it.close();

		if (keepState)
		{
			numLoadedPoints = localNumLoadedPoints;
			firstSeenLocationId = localFirstSeenLocationId;
			lastSeenLocationId = localLastSeenLocationId;
		}

		// Always keep the sampling frequency - if it changes we'll do a full reload
		// above anyway.
		lastSamplingFrequency = pointSamplingFrequency;

		for (TrackDataListener listener : sampledListeners)
		{
			listener.onNewTrackPointsDone();

			// Update the listener state
			ListenerRegistration registration = dataListeners.getRegistration(listener);
			if (registration != null)
			{
				registration.lastTrackId = currentSelectedTrackId;
				registration.lastPointId = localLastSeenLocationId;
				registration.lastSamplingFrequency = pointSamplingFrequency;
				registration.numLoadedPoints = localNumLoadedPoints;
			}
		}
	}

	private void notifyNewPoint(Location location, long locationId, long lastStoredLocationId, int loadedPoints,
			int pointSamplingFrequency, Set<TrackDataListener> sampledListeners, Set<TrackDataListener> sampledOutListeners)
	{
		boolean isValid = LocationUtils.isValidLocation(location);
		if (!isValid)
		{
			// Invalid points are segment splits - report those separately.
			// TODO: Always send last valid point before and first valid point after a
			// split
			for (TrackDataListener listener : sampledListeners)
			{
				listener.onSegmentSplit();
			}
			return;
		}

		// Include a point if it fits one of the following criteria:
		// - Has the mod for the sampling frequency (includes first point).
		// - Is the last point and we are not recording this track.
		boolean recordingSelected = isRecordingSelected();
		boolean includeInSample = (loadedPoints % pointSamplingFrequency == 0 || (!recordingSelected && locationId == lastStoredLocationId));

		if (!includeInSample)
		{
			for (TrackDataListener listener : sampledOutListeners)
			{
				listener.onSampledOutTrackPoint(location);
			}
		}
		else
		{
			// Point is valid and included in sample.
			for (TrackDataListener listener : sampledListeners)
			{
				// No need to allocate a new location (we can safely reuse the
				// existing).
				listener.onNewTrackPoint(location);
			}
		}
	}

	// @VisibleForTesting
	protected void runInListenerThread(Runnable runnable)
	{
		if (listenerHandler == null)
		{
			// Use a Throwable to ensure the stack trace is logged.
			//Log.e(TAG, "Tried to use listener thread before start()", new Throwable());
			return;
		}

		listenerHandler.post(runnable);
	}

	private Set<TrackDataListener> getListenersFor(ListenerDataType type)
	{
		synchronized (dataListeners)
		{
			return dataListeners.getListenersFor(type);
		}
	}

	private EnumSet<ListenerDataType> getNeededListenerTypes()
	{
		EnumSet<ListenerDataType> neededTypes = dataListeners.getAllRegisteredTypes();

		// We always want preference updates.
		neededTypes.add(ListenerDataType.DISPLAY_PREFERENCES);

		return neededTypes;
	}


	public void Destroy()// for baidu map BMapManager
	{
		//if(dataSources != null)
		//dataSources.Destroy();
	}
}
